/**
 * 如何才能知道属性被修改了(或被设置了): 通过Object.defineProperty 函数，通过该函数为对象的每个属性设置一对
 *  getter/setter 从而得知属性被读取和被设置。
 *  但属性的依赖如何收集和处理？
 *  在获取属性的时候收集依赖，然后在设置属性的时候触发之前收集的依赖，此时需要一个筐来存储收集的依赖。
 *  $watch 函数接收两个参数，第一个参数是一个字符串，即数据字段名,比如 'a'，第二个参数是依赖该字段的函数,
 *  且$watch 函数是知道当前正在观测的是哪一个字段的
 */


// 定义一个待属性观察的对象
const data = {
  a: 1,
  b: 1
}
// dep 数组就是我们所谓的“筐”, 循环可以用来处理多个字段（遍历也只能处理第一层的字段 ）
const dep = [];
for (const key in data){
  Object.defineProperty(data, key, {
    set (newVal) {
      // 如果值没有变什么都不做
      if (newVal === val) {
        return false;
      }
      // 有变化则使用新值替换旧值
      val = newVal
      // 当属性被设置的时候，将“筐”里的依赖都执行一次
      dep.forEach(fn => fn())
    },
    get () {
      // 当属性被获取的时候，把依赖放到“筐”里（/ 此时 Target 变量中保存的就是依赖函数）
      dep.push(Target);
      return val  // 将该值返回
    }
  });
}
// Target 是全局变量
let Target = null
function $watch (exp, fn) {
  // 将 Target 的值设置为 fn
  Target = fn
  // 读取字段值，触发 get 函数
  data[exp]
}
/**
 * 定义了全局变量 Target，然后在 $watch 中将 Target 的值设置为 fn 也就是依赖，接着读取字段的值 data[exp] 从而触发被设置的属性的
 * get 函数，在 get 函数中，由于此时 Target 变量就是我们要收集的依赖，所以将 Target 添加到 dep 数组。
 */
// 测试：在设置 data.a = 3时，控制台将分别打印字符串 '第一个依赖' 和 '第二个依赖'
$watch('a', () => {
  console.log('第一个依赖')
})
$watch('a', () => {
  console.log('第二个依赖')
})

/**
 * 为使得程序可以支持多层级对象，可以通过递归处理
 */
const data = {
  a: {
    b: 1
  }
}
function walk (data) {
  for (let key in data) {
    const dep = []
    let val = data[key]
    // 如果 val 是对象，递归调用 walk 函数将其转为访问器属性
    const nativeString = Object.prototype.toString.call(val)
    if (nativeString === '[object Object]') {
      walk(val)
    }
    Object.defineProperty(data, key, {
      set (newVal) {
        if (newVal === val) return
        val = newVal
        dep.forEach(fn => fn())
      },
      get () {
        dep.push(Target)
        return val
      }
    })
  }
}
// $watch('a.b', fn) 这样调用 $watch 函数，那么 data[exp] 等价于 data['a.b']，但实际应该是data['a']['b']
function $watch (exp, fn) {
  Target = fn
  let pathArr,
      obj = data
  // 检查 exp 中是否包含 .
  if (/\./.test(exp)) {
    // 将字符串转为数组，例：'a.b' => ['a', 'b']
    pathArr = exp.split('.')
    // 使用循环读取到 data.a.b
    pathArr.forEach(p => {
      obj = obj[p]
    })
    return
  }
  data[exp]
}
walk(data)

/********************************************监测对象是函数的情形***************************************************************/
const data = {
  name: '霍春阳',
  age: 24
}
function render () {
  return document.write(`姓名：${data.name}; 年龄：${data.age}`)
}
$watch(render, render);
// 为了能够保证 $watch 函数正常执行，我们需要对 $watch 函数做如下修改：
function $watch (exp, fn) {
  Target = fn
  let pathArr,
      obj = data
  // 如果 exp 是函数，直接执行该函数：直接执行该函数会导致依赖二次收集，同时如果监测数组的情况下：也未进行处理。
  if (typeof exp === 'function') {
    exp()
    return
  }
  if (/\./.test(exp)) {
    pathArr = exp.split('.')
    pathArr.forEach(p => {
      obj = obj[p]
    })
    return
  }
  data[exp]
}



